理解简单模型中的 Tree SHAP
一个特征的 SHAP 值,是在所有可能的特征排序中,逐一引入特征时,以该特征为条件,模型输出的平均变化。虽然这个陈述很简单,但计算起来却很有挑战性。因此,本笔记旨在提供一些简单的示例,以便我们了解在非常小的树中这是如何运作的。对于任意大的树,通过观察树来直观地猜测这些值是非常困难的。
[1]:
import graphviz
import numpy as np
import pandas as pd
from sklearn.tree import DecisionTreeRegressor, export_graphviz
import shap
单次分裂示例
[2]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
y[: N // 2] = 1
# fit model
single_split_model = DecisionTreeRegressor(max_depth=1)
single_split_model.fit(X, y)
# draw model
dot_data = export_graphviz(
single_split_model,
out_file=None,
filled=True,
rounded=True,
special_characters=True,
)
graph = graphviz.Source(dot_data)
graph
[2]:
解释模型
请注意,偏置项是模型在训练数据集上的期望输出(0.5)。模型中未使用的特征的 SHAP 值始终为 0,而对于 \(x_0\),其 SHAP 值就是期望值与模型输出之间的差值。
[3]:
xs = [np.ones(M), np.zeros(M)]
df = pd.DataFrame()
for idx, x in enumerate(xs):
index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
df = pd.concat(
[
df,
pd.DataFrame(
[x, shap.TreeExplainer(single_split_model).shap_values(x)],
index=index,
columns=["x0", "x1", "x2", "x3"],
),
]
)
df
[3]:
x0 | x1 | x2 | x3 | ||
---|---|---|---|---|---|
示例 0 | x | 1.0 | 1.0 | 1.0 | 1.0 |
shap_values | 0.5 | 0.0 | 0.0 | 0.0 | |
示例 1 | x | 0.0 | 0.0 | 0.0 | 0.0 |
shap_values | -0.5 | 0.0 | 0.0 | 0.0 |
双特征 AND 示例
我们在此示例中使用两个特征。如果特征 \(x_{0} = 1\) 并且 \(x_{1} = 1\),则目标值为 1,否则为 0。因此,我们称之为 AND 模型。
[4]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: 1 * N // 4, 1] = 1
X[: N // 2, 0] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: 1 * N // 4] = 1
# fit model
and_model = DecisionTreeRegressor(max_depth=2)
and_model.fit(X, y)
# draw model
dot_data = export_graphviz(and_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph
[4]:
解释模型
请注意,偏置项是模型在训练数据集上的期望输出(0.25)。未使用的特征 \(x_2\) 和 \(x_3\) 的 SHAP 值始终为 0。对于 \(x_0\) 和 \(x_1\),其 SHAP 值就是期望值(0.25)与模型输出之间的差值在它们之间平均分配(因为它们对 AND 函数的贡献相同)。
[5]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])] # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
df = pd.concat(
[
df,
pd.DataFrame(
[x, shap.TreeExplainer(and_model).shap_values(x)],
index=index,
columns=["x0", "x1", "x2", "x3"],
),
]
)
df
[5]:
x0 | x1 | x2 | x3 | ||
---|---|---|---|---|---|
示例 0 | x | 1.000 | 1.000 | 1.0 | 1.0 |
shap_values | 0.375 | 0.375 | 0.0 | 0.0 | |
示例 1 | x | 0.000 | 0.000 | 0.0 | 0.0 |
shap_values | -0.125 | -0.125 | 0.0 | 0.0 |
[6]:
y.mean()
[6]:
np.float64(0.25)
以下是示例 1 的 Shap 值的计算过程:偏置项 (y.mean()
) 是 0.25,目标值是 1。这剩下 1 - 0.25 = 0.75 需要在相关特征之间分配。由于只有 \(x_0\) 和 \(x_1\) 对目标值有贡献(且贡献程度相同),因此将 0.75 在它们之间平分,即每个特征分得 0.375。
双特征 OR 示例
我们对上面的示例做一个小小的改动。如果 \(x_{0} = 1\) 或 \(x_{1} = 1\),则目标值为 1,否则为 0。您能在不向下滚动的情况下猜出 SHAP 值吗?
[7]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: N // 2] = 1
y[N // 2 : 3 * N // 4] = 1
# fit model
or_model = DecisionTreeRegressor(max_depth=2)
or_model.fit(X, y)
# draw model
dot_data = export_graphviz(or_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph
[7]:
解释模型
请注意,偏置项是模型在训练数据集上的期望输出(0.75)。模型中未使用的特征的 SHAP 值始终为 0,而对于 \(x_0\) 和 \(x_1\),其 SHAP 值就是期望值与模型输出之间的差值在它们之间平均分配(因为它们对 OR 函数的贡献相同)。
[8]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])] # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
df = pd.concat(
[
df,
pd.DataFrame(
[x, shap.TreeExplainer(or_model).shap_values(x)],
index=index,
columns=["x0", "x1", "x2", "x3"],
),
]
)
df
[8]:
x0 | x1 | x2 | x3 | ||
---|---|---|---|---|---|
示例 0 | x | 1.000 | 1.000 | 1.0 | 1.0 |
shap_values | 0.125 | 0.125 | 0.0 | 0.0 | |
示例 1 | x | 0.000 | 0.000 | 0.0 | 0.0 |
shap_values | -0.375 | -0.375 | 0.0 | 0.0 |
双特征 XOR 示例
[9]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[1 * N // 4 : N // 2] = 1
y[N // 2 : 3 * N // 4] = 1
# fit model
xor_model = DecisionTreeRegressor(max_depth=2)
xor_model.fit(X, y)
# draw model
dot_data = export_graphviz(xor_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph
[9]:
解释模型
请注意,偏置项是模型在训练数据集上的期望输出(0.5)。模型中未使用的特征的 SHAP 值始终为 0,而对于 \(x_0\) 和 \(x_1\),其 SHAP 值就是期望值与模型输出之间的差值在它们之间平均分配(因为它们对 XOR 函数的贡献相同)。
[10]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])] # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
df = pd.concat(
[
df,
pd.DataFrame(
[x, shap.TreeExplainer(xor_model).shap_values(x)],
index=index,
columns=["x0", "x1", "x2", "x3"],
),
]
)
df
[10]:
x0 | x1 | x2 | x3 | ||
---|---|---|---|---|---|
示例 0 | x | 1.00 | 1.00 | 1.0 | 1.0 |
shap_values | -0.25 | -0.25 | 0.0 | 0.0 | |
示例 1 | x | 0.00 | 0.00 | 0.0 | 0.0 |
shap_values | -0.25 | -0.25 | 0.0 | 0.0 |
双特征 AND + 特征提升示例
[11]:
# build data
N = 100
M = 4
X = np.zeros((N, M))
X.shape
y = np.zeros(N)
X[: N // 2, 0] = 1
X[: 1 * N // 4, 1] = 1
X[N // 2 : 3 * N // 4, 1] = 1
y[: 1 * N // 4] = 1
y[: N // 2] += 1
# fit model
and_fb_model = DecisionTreeRegressor(max_depth=2)
and_fb_model.fit(X, y)
# draw model
dot_data = export_graphviz(and_fb_model, out_file=None, filled=True, rounded=True, special_characters=True)
graph = graphviz.Source(dot_data)
graph
[11]:
解释模型
请注意,偏置项是模型在训练数据集上的期望输出(0.75)。模型中未使用的特征的 SHAP 值始终为 0,而对于 \(x_0\) 和 \(x_1\),其 SHAP 值是期望值与模型输出之间的差值在它们之间平均分配(因为它们对 AND 函数的贡献相同),再加上 \(x_0\) 的额外 0.5 影响,因为它本身就具有 \(1.0\) 的影响(如果它为 on,则为 +0.5;如果为 off,则为 -0.5)。
[12]:
xs = np.array([np.ones(M), np.zeros(M)])
# np.array([np.ones(M), np.zeros(M), np.array([1, 0, 1, 0]), np.array([0, 1, 0, 0])] # you can also check these examples
df = pd.DataFrame()
for idx, x in enumerate(xs):
index = pd.MultiIndex.from_product([[f"Example {idx}"], ["x", "shap_values"]])
df = pd.concat(
[
df,
pd.DataFrame(
[x, shap.TreeExplainer(and_fb_model).shap_values(x)],
index=index,
columns=["x0", "x1", "x2", "x3"],
),
]
)
df
[12]:
x0 | x1 | x2 | x3 | ||
---|---|---|---|---|---|
示例 0 | x | 1.000 | 1.000 | 1.0 | 1.0 |
shap_values | 0.875 | 0.375 | 0.0 | 0.0 | |
示例 1 | x | 0.000 | 0.000 | 0.0 | 0.0 |
shap_values | -0.625 | -0.125 | 0.0 | 0.0 |